package org.xx.sipahi.controllers.portal;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.xx.armory.commons.ForLogging;
import org.xx.armory.spring5.data.PageArgs;
import org.xx.armory.spring5.mvc.CacheControlDirective;
import org.xx.armory.spring5.mvc.OutputCache;
import org.xx.sipahi.Category;
import org.xx.sipahi.jpa.CategoryRepository;
import org.xx.sipahi.jpa.NoticeContentRepository;
import org.xx.sipahi.jpa.NoticePO;
import org.xx.sipahi.jpa.NoticePO_;
import org.xx.sipahi.jpa.NoticeRepository;
import org.xx.sipahi.jpa.PresentationContentPO;
import org.xx.sipahi.jpa.PresentationContentRepository;
import org.xx.sipahi.jpa.PresentationPO;
import org.xx.sipahi.jpa.PresentationPO_;
import org.xx.sipahi.jpa.PresentationRepository;

import javax.persistence.criteria.JoinType;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import static java.util.Collections.emptyList;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.xx.armory.spring5.mvc.ModelUtils.paramNonNegative;
import static org.xx.sipahi.jpa.CategoryRepository.toCategory;
import static org.xx.sipahi.jpa.NoticeRepository.toNotice;
import static org.xx.sipahi.jpa.PresentationRepository.toPresentation;

/**
 * 用于显示前台页面。
 */
@Controller
public class PageController
        extends PortalController {
    private static final String VIEW_ENCODING = "utf-8";

    @ForLogging
    private final Logger logger = LoggerFactory.getLogger(PageController.class);

    @Autowired
    private CategoryRepository categoryRepository;

    @Autowired
    private NoticeRepository noticeRepository;

    @Autowired
    private NoticeContentRepository noticeContentRepository;

    @Autowired
    private PresentationRepository presentationRepository;

    @Autowired
    private PresentationContentRepository presentationContentRepository;

    @Autowired
    private FreeMarkerRenderer freemarkerRenderer;

    private void addPages(
            Map<String, ? super Object> model,
            Category category,
            int pageNumber
    ) {
        final var pageArgs = PageArgs.of((pageNumber - 1) * category.getPageSize(), category.getPageSize());
        model.put("pageNumber", pageNumber);
        final var categoryPO = this.categoryRepository.getOneOrNull(category.getId());
        switch (category.getModule()) {
            case 1:
                final var noticePage = this.noticeRepository.findAll(
                        (root, cq, cb) -> {
                            if (NoticePO.class.isAssignableFrom(cq.getResultType())) {
                                root.fetch(NoticePO_.category, JoinType.LEFT);
                            }

                            return cb.equal(root.get(NoticePO_.category), categoryPO);
                        }, pageArgs
                );
                model.put("pages", noticePage.toList());
                model.put("pageCount", noticePage.getTotalPages());
                break;
            case 2:
                final var presentationPage = this.presentationRepository.findAll(
                        (root, cq, cb) -> {
                            if (PresentationPO.class.isAssignableFrom(cq.getResultType())) {
                                root.fetch(PresentationPO_.category, JoinType.LEFT);
                            }

                            return cb.equal(root.get(PresentationPO_.category), categoryPO);
                        }, pageArgs
                );
                model.put("pages", presentationPage.toList());
                model.put("pageCount", presentationPage.getTotalPages());
            default:
                logger.warn("Unknown module {} of category {}", category.getModule(), category.getId());
                model.put("pages", emptyList());
        }
    }

    @GetMapping("/categories/{idAndPage}")
    @Transactional(readOnly = true)
    public void renderCategory(
            @PathVariable String idAndPage,
            @RequestHeader(value = "If-Modified-Since",
                           required = false) Date date,
            HttpServletRequest request,
            HttpServletResponse response
    ) {
        final var categoryIdAndPage = parseIdAndPage(idAndPage);
        final var categoryId = categoryIdAndPage[0];
        final var pageNumber = categoryIdAndPage[1];
        final var model = new HashMap<String, Object>();
        final var category = toCategory(this.categoryRepository.findById(categoryId).orElse(null));
        try {
            if (category != null) {
                switch (checkUser(model, request, category)) {
                    case OK:
                        model.put("id", category.getId());
                        model.put("title", category.getTitle());
                        model.put("description", category.getDescription());
                        final var module = category.getModule();
                        model.put("module", module);

                        switch (module) {
                            case 1:
                                addPages(model, category, pageNumber);

                                render(category.getTpl1(), model, request, response);
                                break;
                            case 2:
                                final var indexPageId = this.presentationRepository.findIndex(category.getId());
                                if (indexPageId != null) {
                                    render(module, indexPageId, date, request, response);
                                } else {
                                    send404(request, response);
                                }
                                break;
                            default:
                                break;
                        }
                        break;
                    case UNAUTHORIZED:
                        send401(request, response);
                        break;
                    case FORBIDDEN:
                        send403(request, response);
                        break;
                    default:
                        break;
                }
            } else {
                send404(request, response);
            }
        } catch (RenderException ex) {
            logger.error("Cannot write to http response", ex);
        } catch (Exception ex) {
            logger.error("Internal server error", ex);
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    private boolean before(
            Date date,
            Date lastModified
    ) {
        return date == null || lastModified == null || date.before(lastModified);
    }

    @GetMapping("/pages/{module}/{id:^\\d+$}")
    @OutputCache(directive = CacheControlDirective.PUBLIC,
                 maxAge = 60 * 60)
    @Transactional(readOnly = true)
    public void render(
            @PathVariable int module,
            @PathVariable long id,
            @RequestHeader(value = "If-Modified-Since",
                           required = false) Date date,
            HttpServletRequest request,
            HttpServletResponse response
    ) {
        paramNonNegative(id, "id");
//        paramNotBlank(hash, "hash");

        final var model = new HashMap<String, Object>();
        try {
            switch (module) {
                case 1:
                    final var notice = toNotice(
                            this.noticeRepository.findById(id).orElse(null),
                            this.noticeContentRepository.findById(id).orElse(null)
                    );
                    if (notice != null) {
                        if (before(date, notice.getLastModifiedTime())) {
                            switch (checkUser(model, request, notice.getCategory())) {
                                case OK:
                                    model.put("id", notice.getId());
                                    model.put("title", notice.getTitle());
                                    model.put("brief", notice.getBrief());
                                    model.put("keywords", notice.getKeywords());
                                    model.put("lastModifiedTime", notice.getLastModifiedTime());
                                    model.put("star", notice.isStar());
                                    model.put("stuck", notice.getStickTime() != null);
                                    model.put("content", notice.getContent());
                                    model.put("category", notice.getCategory());

                                    render(notice.getCategory().getTpl2(), model, request, response);
                                    break;
                                case UNAUTHORIZED:
                                    send401(request, response);
                                    break;
                                case FORBIDDEN:
                                    send403(request, response);
                                    break;
                                default:
                                    break;
                            }
                        } else {
                            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                        }
                    } else {
                        send404(request, response);
                    }
                    break;
                case 2:
                    final var presentation = toPresentation(
                            this.presentationRepository.findById(id).orElse(null),
                            this.presentationContentRepository.findById(id).orElse(null)
                    );
                    if (presentation != null) {
                        if (before(date, presentation.getLastModifiedTime())) {
                            switch (checkUser(model, request, presentation.getCategory())) {
                                case OK:
                                    model.put("id", presentation.getId());
                                    model.put("title", presentation.getTitle());
                                    model.put("brief", presentation.getBrief());
                                    model.put("keywords", presentation.getKeywords());
                                    model.put("lastModifiedTime", presentation.getLastModifiedTime());
                                    model.put("content", this.presentationContentRepository.findById(id).map(PresentationContentPO::getContent).orElse(""));
                                    model.put("category", presentation.getCategory());

                                    render(presentation.getCategory().getTpl2(), model, request, response);
                                    break;
                                case UNAUTHORIZED:
                                    send401(request, response);
                                    break;
                                case FORBIDDEN:
                                    send403(request, response);
                                    break;
                                default:
                                    break;
                            }
                        } else {
                            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                        }
                    } else {
                        send404(request, response);
                    }
                    break;
                default:
                    logger.warn("Unknown module {}", module);
                    send404(request, response);
                    break;
            }
        } catch (RenderException ex) {
            logger.error("Cannot write to http response", ex);
        } catch (Exception ex) {
            logger.error("Internal server error", ex);
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    private void render(
            String tpl,
            HashMap<String, Object> model,
            HttpServletRequest request,
            HttpServletResponse response
    )
            throws RenderException {
        if (!isBlank(tpl)) {
            try {
                renderMergedTemplateModel(model, tpl, request, response);
            } catch (IOException ex) {
                throw new RenderException(ex);
            }
        } else {
            send204(request, response);
        }
    }

    private void renderMergedTemplateModel(
            Map<String, Object> model,
            String tpl,
            HttpServletRequest request,
            HttpServletResponse response
    )
            throws IOException {
        response.setCharacterEncoding(VIEW_ENCODING);

        freemarkerRenderer.render(tpl, model, VIEW_ENCODING, request, response);

        response.flushBuffer();
    }
}
